Skip to content

Let spm deinit take an app back to CocoaPods - #59368

Merged
meta-codesync[bot] merged 4 commits into
mainfrom
chrfalch/spm-cocoapods-opt-out
Nov 3, 2026
Merged

meta-codesync[bot] merged 4 commits into
mainfrom
chrfalch/spm-cocoapods-opt-out

Conversation

@chrfalch

@chrfalch chrfalch commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary:

An app converted to SwiftPM cannot go back to CocoaPods today. The conversion deletes the React Native lines from the Podfile, one line at a time. A call that spans several lines, such as use_react_native!(…), keeps its other lines, so the Podfile is no longer valid Ruby.

This PR keeps the Podfile unchanged. The way back to CocoaPods is then npx react-native spm deinit, followed by pod install.

What changes:

  1. spm add --deintegrate keeps the Podfile. It runs pod deintegrate and nothing else on the Podfile.
  2. Plain npx react-native spm no longer converts a CocoaPods app. It stops and points to spm add --deintegrate.
  3. pod install stops on a SwiftPM app. The Podfile is still there, so a user can run pod install by mistake. That would add CocoaPods back and break the generated Package.swift. The React Native CocoaPods scripts now stop with an error, before they write any file, when the .xcodeproj has the SwiftPM marker file (.spm-injected.json).
  4. spm deinit cleans up after itself. It removes the .gitignore lines that the conversion added, and the empty ReactNativeSPM folders. Then it says to run pod install.

Smaller fixes from the review:

  • The uncommitted-changes prompt now comes before pod deintegrate, so "no" changes nothing.
  • .gitignore is written only after a successful conversion.
  • Pods that come from node_modules/react-native no longer show in the "pods that are not part of React Native" warning.

The three SwiftPM iOS CI workflows checked the Podfile content after the conversion. They now check that the .xcodeproj has no CocoaPods references.

Where to look:

  • scripts/setup-apple-spm.js: the conversion and deinit (items 1, 2, 4).
  • scripts/cocoapods/utils.rb (error_if_spm_injected), called from react_native_pods.rb and cocoapods/autolinking.rb: the pod install check (item 3).

Changelog:

[IOS] [CHANGED] - react-native spm add --deintegrate keeps the Podfile, and pod install stops with an error on an app that uses SwiftPM. react-native spm deinit and then pod install take an app back to CocoaPods.

Test Plan:

Unit tests:

yarn jest packages/react-native/scripts/spm/__tests__

25 suites, 1236 tests pass.

A new app on React Native 0.89.0-nightly-20261021-c7429b0e9 and CLI 21.0.0-alpha.1, with the changed scripts copied in:

  1. npx react-native spm add --deintegrate --yes: the app converts, the Podfile is unchanged, and the SwiftPM build succeeds.
  2. pod install: it stops with "This app uses Swift Package Manager for React Native … so pod install would break the Xcode project." No file changes.
  3. npx react-native run-ios and build-ios: both build the app. The CLI sees the marker file and does not run pod install.
  4. npx react-native spm deinit, then pod install: the CocoaPods build succeeds.

Not checked: Flow locally (my Flow binary is too old), run-ios --force-pods, and CLI 20.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 22, 2026
@chrfalch
chrfalch requested a review from cipolleschi October 22, 2026 16:31
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 22, 2026
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Visual regression

1 visual difference(s) at dda80a2cf86c. Visual differences need maintainer approval. Review the report, then apply visual-change-approved.

ios view/z-index/reordered: Changed, 15396 px (2.160%)

diff

Expected and Captured
Expected Captured
expected captured

Converting an app to SwiftPM with `--deintegrate` now moves the Podfile
to Podfile.bak instead of deleting React Native lines from it (that edit
left invalid Ruby for multi-line calls). `spm deinit` restores the
Podfile, undoes the .gitignore lines the conversion wrote, removes empty
ReactNativeSPM folders, and says to run `pod install`.

The conversion refuses, before changing anything, when the Podfile has
pods that are not React Native or when Podfile.bak already exists.
The Podfile is moved to Podfile.bak, so no pod is lost and `spm deinit`
restores it. An explicit `--deintegrate` now lists the other pods, says
CocoaPods will not install them while the app uses SwiftPM unless SwiftPM
provides them, and continues. RNTester needs this: its Podfile has local
example pods that SwiftPM provides.

The automatic conversion of a new app still skips such Podfiles.
@chrfalch
chrfalch force-pushed the chrfalch/spm-cocoapods-opt-out branch from b75a2b8 to dda80a2 Compare October 28, 2026 14:06

@cipolleschi cipolleschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this!
I'd changes some bits, to simplify the feature... See my comments!

```

On a fresh CocoaPods app, this converts the app to SwiftPM. It runs
`pod deintegrate` and moves the `Podfile` to `Podfile.bak`. A Podfile with pods

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Podfile can remain Podfile. Why do we need to change it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The Podfile now stays as it is.

`pod deintegrate` and moves the `Podfile` to `Podfile.bak`. A Podfile with pods
that are not part of React Native is not converted automatically;
`spm add --deintegrate` converts it and warns that those pods are not installed.
Without a Podfile, the CLI needs `@react-native-community/cli` 21 or newer to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we keep it, we can lift this requirement

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The CLI 21 requirement is gone, because the Podfile stays in place.

Comment on lines +46 to +48
2. moves the `Podfile` to `Podfile.bak`, unchanged. `spm deinit` moves it back.
If `Podfile.bak` already exists, the command stops before it changes
anything.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not do this step.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The step is removed.

Non-RN pods can stay side-by-side. Your Podfile still lists them. Remove the
leftover React Native lines (see step 2 above), then re-integrate the pods with
a normal install:
`--deintegrate` converts an app whose Podfile has pods that are not part of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be confusing that we have two different behaviors for spm --deintegrate and spm.
I understand that we would like to remove a step (manually calling --deintegrate) but perhaps it is not a bad thing that the user has to call it themselves.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Plain spm no longer runs --deintegrate. On a CocoaPods app it stops and points to spm add --deintegrate.

[local native module](#local-native-modules). The pods stay in `Podfile.bak`,
and `spm deinit` restores the Podfile.

To keep a CocoaPods-only pod next to SwiftPM, convert by hand:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there Cocoapods-only pods? I believe that most Native iOS libraries nowadays supports Swift PM. For React Native packages, we should strive to auto-convert them or to fail. If the users have a pod that still needs to be converted to SwiftPM and that can't be converted automatically, I would probably fail and ask them to open a PR/Issue to convert the library.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept this a warning, as in my top-level comment (point 4). The Podfile now stays unchanged, so spm deinit and then pod install bring those pods back. An error would also block RNTester: its Podfile has local pods that SwiftPM provides through spmModules.

return true;
}

function restorePodfile(appRoot /*: string */) /*: void */ {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The move isn't recorded in the marker (unlike the .gitignore lines), so deinit renames any Podfile.bak to Podfile, including one the user made themselves. This goes away if we keep the Podfile in place.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gone. The Podfile.bak code is removed.

return false;
}
if (podfileHasThirdPartyPods(appRoot)) {
if (podfileHasThirdPartyPods(appRoot) || hasPodfileBackup(appRoot)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When Podfile.bak exists, this skips without saying why. The fail-fast check then suggests spm add --deintegrate, which refuses for the same reason. Could we log the reason here?

If we get rid of the Podfile.bak, we can simplify the code significantly

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gone. The Podfile.bak code is removed.

function podfileHasThirdPartyPods(appRoot /*: string */) /*: boolean */ {
// The names of the pods the Podfile declares explicitly (`pod '...'` or
// `pod('...')`), deduplicated, in order. React Native itself comes in through
// use_react_native!, so every name here is a pod that is not part of it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Older or brownfield Podfiles can list RN pods explicitly (e.g. pod 'React-Core', :path => '../node_modules/react-native/'), so this would report them as non-RN. Pods from test targets also end up in the list.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Pods with :path or :podspec into node_modules/react-native now count as React Native pods. Pods in test targets stay in the list: to find them, we would need to parse the Ruby target blocks.

const podNames = podfilePodNames(appRoot);
if (podNames.length > 0) {
logError(
'Your Podfile declares pods that are not part of React Native: ' +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is a warning, but it goes through logError and the run continues. If we decide to fail here instead (see my comment in the docs), this is the place to do it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. It is now printed as a warning.

function runDeintegrate(appRoot /*: string */) /*: void */ {
const podfilePath = path.join(appRoot, 'Podfile');
const backupPath = path.join(appRoot, 'Podfile.bak');
if (refuseToOverwritePodfileBackup(appRoot)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: main already runs this check before we get here, so this one never fires on that path. Is it only meant for direct callers?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gone. This check was part of the Podfile.bak code.

@chrfalch

Copy link
Copy Markdown
Collaborator Author

Thanks for the review, Riccardo! I agree with most of it. My plan:

  1. Keep the Podfile in place. --deintegrate runs pod deintegrate and leaves the Podfile unchanged. All the Podfile.bak code goes, and with it the bugs you found around it and the need for CLI 21. The way back is then spm deinit followed by pod install. To catch a pod install run by mistake, the next spm run or Xcode sync stops with a clear error when CocoaPods is back in the project.
  2. Plain npx react-native spm no longer converts a CocoaPods app. The user, or the CLI, runs spm add --deintegrate explicitly.
  3. Order of steps: the uncommitted-changes prompt moves before any change, and .gitignore is written only after a successful injection.
  4. Other pods: I would like to keep this a warning, not an error. The Podfile is now kept unchanged, so nothing is lost. An error would also block RNTester: its Podfile has local pods that SwiftPM provides through spmModules. I will fix the two problems you found: React Native pods that are listed by name now count as React Native pods, and the message is printed as a warning.
  5. deinit → remove: I would leave the name as it is in this PR. deinit already exists, and other tools call these scripts directly. Happy to do the rename in a separate PR if you want it.

I will push these as new commits.

Review feedback: `spm add --deintegrate` no longer moves the Podfile to
Podfile.bak. It runs `pod deintegrate` and keeps the Podfile unchanged, so
`spm deinit` and then `pod install` go back to CocoaPods.

- Plain `spm` no longer converts a CocoaPods app; it points to
  `spm add --deintegrate`.
- The React Native CocoaPods scripts stop `pod install` before any write when
  the .xcodeproj next to the Podfile has .spm-injected.json. Otherwise
  `pod install` rewrites the generated Package.swift with a wrong path and
  Xcode fails to resolve packages before any build phase runs.
- The uncommitted-changes prompt comes before `pod deintegrate`.
- .gitignore is written only after a successful injection.
- Pods sourced from node_modules/react-native count as React Native pods,
  and the other-pods message is a warning.
- The SwiftPM iOS workflows check for `Pods-` references in the pbxproj
  instead of the Podfile content.
@chrfalch chrfalch changed the title Let spm deinit take an app back to CocoaPods Keep the Podfile so spm deinit can take an app back to CocoaPods Oct 30, 2026
@chrfalch chrfalch changed the title Keep the Podfile so spm deinit can take an app back to CocoaPods Let spm deinit take an app back to CocoaPods Oct 30, 2026
@chrfalch

Copy link
Copy Markdown
Collaborator Author

Pushed the changes in 0640f08. One change from my earlier plan (point 1): a mistaken pod install is now stopped by pod install itself. The React Native CocoaPods scripts see the SwiftPM marker in the .xcodeproj and stop with an error before they write any file. A later check in spm or the Xcode sync phase was too late: pod install rewrites the generated Package.swift, and Xcode fails to resolve packages before any build phase runs. The test plan in the description has the details.

for (const line of fs.readFileSync(podfilePath, 'utf8').split('\n')) {
// `#{` is Ruby string interpolation, not a comment. A line that ends in
// `,` continues on the next one.
const code = continued + line.split(/#(?!\{)/)[0].trimEnd();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small edge case: continued ends with a space, so if a blank or comment-only line sits in the middle of a multi-line pod call, code.endsWith(',') is false and the statement ends before its :path. For example:

pod 'React-Core',
  # comment
  :path => '../node_modules/react-native/'

This gets reported as a non-RN pod. Trimming the whole code string should fix it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in dae2c23. The whole joined line is trimmed now, so a blank or comment line inside a pod call no longer ends it. I added a test with both cases.

);
}
if (fs.existsSync(path.join(appRoot, 'Podfile'))) {
log('To go back to CocoaPods, run `pod install`.');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is printed even when deinit found nothing to remove. Also, when the command was redirected from the JS root, it might help to say where to run it (e.g. cd ios && pod install, or displayPath(appRoot)).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dae2c23. The line is printed only when deinit removed the SwiftPM setup. When the command was run from the JS root, it says cd ios && pod install. Tests added for both.

- A multi-line `pod` call with a blank or comment line in the middle no
  longer ends early, so a pod sourced from node_modules/react-native is not
  reported as a non-React Native pod.
- `spm deinit` says to run `pod install` only when it removed the SwiftPM
  setup, and says `cd ios && pod install` when run from the JS root.

@cipolleschi cipolleschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing all the feedback!

@meta-codesync

meta-codesync Bot commented Nov 2, 2026

Copy link
Copy Markdown

@cipolleschi has imported this pull request. If you are a Meta employee, you can view this in D129064536.

@meta-codesync
meta-codesync Bot merged commit b255245 into main Nov 3, 2026
192 of 193 checks passed
@react-native-bot

Copy link
Copy Markdown
Collaborator

This pull request was successfully merged by @chrfalch in b255245

When will my fix make it into a release? | How to file a pick request?

@react-native-bot react-native-bot added the Merged This PR has been merged. label Nov 3, 2026
chrfalch added a commit that referenced this pull request Nov 3, 2026
`spm add --deintegrate` keeps the Podfile since #59368, so the RNTester
check now looks for CocoaPods references in RNTesterPods.xcodeproj, as the
SwiftPM workflows on main do.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. p: Expo Partner: Expo Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants